<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SIP Phone(SIP.js version 0.20.0)</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1100px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }

        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        .layout {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }

        .left-panel,
        .right-panel {
            flex: 1 1 320px;
            display: flex;
            flex-direction: column;
            gap: 20px;
        }

        .section {
            margin-bottom: 30px;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }

        .section h3 {
            margin-top: 0;
            color: #333;
        }

        .form-group {
            margin-bottom: 15px;
        }

        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }

        input[type="text"],
        input[type="password"],
        input[type="url"],
        input[type="tel"],
        input[type="number"],
        textarea,
        button {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 100%;
            box-sizing: border-box;
        }

        input[type="checkbox"] {
            width: auto;
            margin-right: 8px;
        }

        input::placeholder,
        textarea::placeholder {
            color: rgba(148, 163, 184, 0.7);
            opacity: 1;
        }

        button {
            background: #007bff;
            color: white;
            cursor: pointer;
            margin-top: 10px;
        }

        button:hover {
            background: #0056b3;
        }

        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }

        label.checkbox-inline {
            display: inline-flex;
            align-items: center;
            gap: 8px;
        }

        .status {
            padding: 10px;
            margin: 10px 0;
            border-radius: 4px;
        }

        .status.success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .status.info {
            background: #d1ecf1;
            color: #0c5460;
            border: 1px solid #bee5eb;
        }

        .call-controls {
            display: none;
        }

        .call-controls.active {
            display: block;
        }

        .logs {
            height: 300px;
            overflow-y: auto;
            background: #f8f9fa;
            padding: 10px;
            border: 1px solid #ddd;
            font-family: monospace;
            font-size: 12px;
        }

        .incoming-call {
            display: none;
            background: #fff3cd;
            border: 1px solid #ffeaa7;
            padding: 20px;
            border-radius: 5px;
            margin: 10px 0;
        }

        .incoming-call.active {
            display: block;
        }

        .call-info {
            margin: 10px 0;
        }

        @media (max-width: 768px) {
            body {
                max-width: 100%;
            }

            .layout {
                flex-direction: column;
            }
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>SIP Phone(SIP.js version 0.20.0)</h1>

        <div class="layout">
            <div class="left-panel">
                <div class="section">
                    <h3>Registration</h3>
                    <div class="form-group">
                        <label for="server">SIP Server (WebSocket URL):</label>
                        <input type="text" id="server" data-default-path="/ws" placeholder="ws://localhost:8080/ws">
                    </div>
                    <div class="form-group">
                        <label for="username">Username:</label>
                        <input type="text" id="username" value="alice" placeholder="alice">
                    </div>
                    <div class="form-group">
                        <label for="password">Password:</label>
                        <input type="password" id="password" value="123456" placeholder="password">
                    </div>
                    <div class="form-group">
                        <label for="displayName">Display Name:</label>
                        <input type="text" id="displayName" value="Alice" placeholder="Alice">
                    </div>
                    <div class="form-group">
                        <label class="checkbox-inline"><input type="checkbox" id="iceEnable" checked> Enable ICE
                            servers</label>
                        <textarea id="iceServers" rows="3"
                            style="width:100%;">[{"urls":"stun:stun.l.google.com:19302"}]</textarea>
                    </div>
                    <button id="registerBtn">Register</button>
                    <button id="unregisterBtn" disabled>Unregister</button>
                    <div id="registrationStatus"></div>
                </div>

                <div class="section">
                    <h3>Logs</h3>
                    <div class="logs" id="logs"></div>
                    <div style="margin-top:8px;">
                        <button onclick="clearLogs()">Clear Logs</button>
                    </div>
                </div>
            </div>

            <div class="right-panel">
                <div class="section">
                    <h3>Make Call</h3>
                    <div class="form-group">
                        <label for="callTarget">Call To:</label>
                        <input type="text" id="callTarget" value="bob" placeholder="bob">
                    </div>
                    <button id="callBtn" disabled>Call</button>

                    <div class="call-controls" id="callControls">
                        <div class="call-info" id="callInfo"></div>
                        <button id="hangupBtn">Hang Up</button>
                        <button id="muteBtn">Mute</button>
                        <button id="holdBtn">Hold</button>
                    </div>
                </div>

                <div class="incoming-call" id="incomingCall">
                    <h3>Incoming Call</h3>
                    <div id="callerInfo"></div>
                    <button id="answerBtn">Answer</button>
                    <button id="rejectBtn">Reject</button>
                </div>

                <div class="section">
                    <h3>ICE Stats</h3>
                    <div class="logs" id="iceStats" style="height:220px;"><em>No ICE data yet.</em></div>
                    <audio id="remoteAudio" autoplay style="display:none;"></audio>
                    <audio id="localAudio" muted autoplay style="display:none;"></audio>
                </div>
            </div>
        </div>
    </div>

    <script src="//cdnjs.cloudflare.com/ajax/libs/sip.js/0.20.0/sip.min.js"></script>
    <script>
        let userAgent = null;
        let registerer = null;
        let currentSession = null;
        let isMuted = false;
        let isOnHold = false;

        // DOM elements
        const serverInput = document.getElementById('server');
        const usernameInput = document.getElementById('username');
        const passwordInput = document.getElementById('password');
        const displayNameInput = document.getElementById('displayName');
        const registerBtn = document.getElementById('registerBtn');
        const unregisterBtn = document.getElementById('unregisterBtn');
        const registrationStatus = document.getElementById('registrationStatus');
        const callTargetInput = document.getElementById('callTarget');
        const callBtn = document.getElementById('callBtn');
        const callControls = document.getElementById('callControls');
        const callInfo = document.getElementById('callInfo');
        const hangupBtn = document.getElementById('hangupBtn');
        const muteBtn = document.getElementById('muteBtn');
        const holdBtn = document.getElementById('holdBtn');
        const incomingCallDiv = document.getElementById('incomingCall');
        const callerInfo = document.getElementById('callerInfo');
        const answerBtn = document.getElementById('answerBtn');
        const rejectBtn = document.getElementById('rejectBtn');
        const remoteAudio = document.getElementById('remoteAudio');
        const localAudio = document.getElementById('localAudio');
        const logs = document.getElementById('logs');
        const iceStatsPanel = document.getElementById('iceStats');
        const iceEnable = document.getElementById('iceEnable');
        const iceServersInput = document.getElementById('iceServers');
        let lastOutgoingCallLabel = null;
        let iceServersManuallyEdited = false;

        // Event listeners
        registerBtn.addEventListener('click', register);
        unregisterBtn.addEventListener('click', unregister);
        callBtn.addEventListener('click', makeCall);
        hangupBtn.addEventListener('click', hangup);
        muteBtn.addEventListener('click', toggleMute);
        holdBtn.addEventListener('click', toggleHold);
        answerBtn.addEventListener('click', answerCall);
        rejectBtn.addEventListener('click', rejectCall);

        if (iceServersInput) {
            iceServersInput.addEventListener('input', () => {
                iceServersManuallyEdited = true;
            });
        }

        const defaultServerUrl = (() => {
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
            const host = window.location.host || 'localhost:8080';
            const pathRaw = (serverInput && serverInput.dataset && serverInput.dataset.defaultPath) || '/ws';
            const path = pathRaw.startsWith('/') ? pathRaw : `/${pathRaw}`;
            return `${protocol}//${host}${path}`;
        })();

        if (serverInput) {
            serverInput.placeholder = defaultServerUrl;
            if (!serverInput.value || serverInput.value === 'ws://localhost:8080/ws' || serverInput.value === 'wss://localhost:8080/ws') {
                serverInput.value = defaultServerUrl;
            }
        }

        function log(message) {
            const timestamp = new Date().toLocaleTimeString();
            logs.innerHTML += `[${timestamp}] ${message}<br>`;
            logs.scrollTop = logs.scrollHeight;
            console.log(message);
        }

        function clearLogs() {
            logs.innerHTML = '';
            resetIceStats();
        }

        function showStatus(message, type = 'info') {
            registrationStatus.className = `status ${type}`;
            registrationStatus.textContent = message;
        }

        function resetIceStats(message = '<em>No ICE data yet.</em>') {
            if (iceStatsPanel) {
                iceStatsPanel.innerHTML = message;
            }
        }

        resetIceStats();

        async function loadDefaultIceServers() {
            if (!iceServersInput || iceServersManuallyEdited) {
                return;
            }
            try {
                const response = await fetch('/iceservers', { cache: 'no-store' });
                if (!response.ok) {
                    throw new Error(`status ${response.status}`);
                }
                const payload = await response.json();
                const servers = Array.isArray(payload)
                    ? payload
                    : (payload && Array.isArray(payload.iceServers))
                        ? payload.iceServers
                        : payload
                            ? [payload]
                            : null;
                if (!servers || !servers.length) {
                    throw new Error('empty ICE server list');
                }
                if (iceServersManuallyEdited) {
                    return;
                }
                iceServersInput.value = JSON.stringify(servers);
                if (iceEnable) {
                    iceEnable.checked = true;
                }
                log('Loaded ICE servers from /iceservers');
            } catch (error) {
                log(`Failed to load /iceservers: ${error.message}`);
            }
        }

        function formatMs(ms) {
            if (ms == null) return '--';
            if (ms < 1000) return ms + ' ms';
            return (ms / 1000).toFixed(2) + ' s';
        }

        function monitorIceGathering(session, label = 'session') {
            try {
                if (!session || session._iceMonitorAttached) return;
                const sdh = session.sessionDescriptionHandler;
                if (!sdh || !sdh.peerConnection) {
                    return;
                }

                const pc = sdh.peerConnection;
                session._iceMonitorAttached = true;

                const statsLabel = session._iceLabel || label;
                session._iceCandidates = [];
                session._iceGatherDuration = null;

                if (iceStatsPanel) {
                    iceStatsPanel.innerHTML = `<div><strong>Call:</strong> ${statsLabel}</div><div><strong>Candidates:</strong> 0</div><div><strong>Status:</strong> waiting for candidates</div>`;
                }

                let start = null;
                let end = null;
                let inactivityTimer = null;

                function renderStats(statusText) {
                    if (!iceStatsPanel) return;
                    const parts = [
                        `<div><strong>Call:</strong> ${statsLabel}</div>`,
                        `<div><strong>Candidates:</strong> ${session._iceCandidates.length}</div>`
                    ];
                    if (session._iceGatherDuration != null) {
                        parts.push(`<div><strong>Gathering time:</strong> ${formatMs(session._iceGatherDuration)}</div>`);
                    }
                    if (statusText) {
                        parts.push(`<div><strong>Status:</strong> ${statusText}</div>`);
                    }
                    if (session._iceCandidates.length) {
                        const rows = session._iceCandidates.map((c, i) => {
                            const addressPort = [c.address || '', c.port ? `:${c.port}` : ''].join('');
                            return `${i + 1}. ${c.type || 'unknown'} ${addressPort.trim()} (+${formatMs(c.ts)})`;
                        });
                        parts.push('<hr>');
                        parts.push(rows.join('<br>'));
                    }
                    iceStatsPanel.innerHTML = parts.join('');
                }

                function concludeGathering(reason) {
                    if (end) return;
                    end = Date.now();
                    session._iceGatherDuration = start ? (end - start) : null;
                    const readableReason = reason ? reason.replace(/_/g, ' ') : 'completed';
                    log(`${statsLabel} - ICE gathering finished (${readableReason})${session._iceGatherDuration != null ? `, duration=${formatMs(session._iceGatherDuration)}` : ''}`);
                    renderStats(readableReason);
                    if (inactivityTimer) {
                        clearTimeout(inactivityTimer);
                        inactivityTimer = null;
                    }
                }

                function parseCandidate(candStr, candObj) {
                    const out = { raw: candStr };
                    try {
                        if (candObj) {
                            out.address = candObj.address || candObj.ip || null;
                            out.port = candObj.port || null;
                            out.type = candObj.type || null;
                            out.protocol = candObj.protocol || null;
                        }
                        if (!out.address && typeof candStr === 'string') {
                            const ipMatch = candStr.match(/ (\d+\.\d+\.\d+\.\d+) (\d+) /);
                            if (ipMatch) {
                                out.address = ipMatch[1];
                                out.port = ipMatch[2];
                            }
                            const typeMatch = candStr.match(/ typ (\w+)/);
                            if (typeMatch) {
                                out.type = typeMatch[1];
                            }
                        }
                    } catch (error) {
                        // parsing best-effort; fall back to raw candidate
                    }
                    return out;
                }

                pc.addEventListener('icecandidate', (event) => {
                    if (event && event.candidate) {
                        const now = Date.now();
                        if (!start) {
                            start = now;
                            log(`${statsLabel} - ICE gathering started`);
                        }
                        const parsed = parseCandidate(event.candidate.candidate, event.candidate);
                        const entry = {
                            ts: start ? now - start : 0,
                            candidate: parsed.raw,
                            type: parsed.type,
                            address: parsed.address,
                            port: parsed.port
                        };
                        session._iceCandidates.push(entry);
                        log(`${statsLabel} - candidate #${session._iceCandidates.length}: ${parsed.type || 'unknown'} ${parsed.address || ''}${parsed.port ? `:${parsed.port}` : ''} (+${formatMs(entry.ts)})`);
                        renderStats('gathering');
                        if (inactivityTimer) clearTimeout(inactivityTimer);
                        inactivityTimer = setTimeout(() => concludeGathering('inactivity timeout'), 2500);
                    } else {
                        if (!end && start) {
                            concludeGathering('null candidate');
                        } else if (!start) {
                            concludeGathering('no candidates');
                        }
                    }
                });

                pc.addEventListener('icegatheringstatechange', () => {
                    const state = pc.iceGatheringState;
                    log(`${statsLabel} - iceGatheringState -> ${state}`);
                    if (state === 'gathering' && !start) {
                        start = Date.now();
                        renderStats('gathering');
                    }
                    if (state === 'complete' && start && !end) {
                        concludeGathering('ice gathering complete');
                    }
                });

                pc.addEventListener('iceconnectionstatechange', () => {
                    const state = pc.iceConnectionState;
                    log(`${statsLabel} - iceConnectionState -> ${state}`);
                    if ((state === 'connected' || state === 'completed') && start && !end) {
                        concludeGathering('ice connection completed');
                    }
                });

                pc.addEventListener('connectionstatechange', () => {
                    const state = pc.connectionState;
                    log(`${statsLabel} - connectionState -> ${state}`);
                });

                renderStats('waiting for candidates');
            } catch (e) {
                log(`monitorIceGathering error: ${e.message}`);
            }
        }

        function setupMediaElements(session) {
            try {
                if (!session.sessionDescriptionHandler || !session.sessionDescriptionHandler.peerConnection) {
                    log('Session description handler not available yet');
                    return;
                }

                const peerConnection = session.sessionDescriptionHandler.peerConnection;

                const remoteStream = new MediaStream();
                peerConnection.getReceivers().forEach(receiver => {
                    if (receiver.track && receiver.track.kind === 'audio') {
                        remoteStream.addTrack(receiver.track);
                    }
                });

                if (remoteStream.getTracks().length > 0) {
                    remoteAudio.srcObject = remoteStream;
                    log('Remote audio stream attached');
                }

                const senders = peerConnection.getSenders();
                if (senders.length > 0) {
                    const localStream = new MediaStream();
                    senders.forEach(sender => {
                        if (sender.track && sender.track.kind === 'audio') {
                            localStream.addTrack(sender.track);
                        }
                    });

                    if (localStream.getTracks().length > 0) {
                        localAudio.srcObject = localStream;
                        log('Local audio stream attached');
                    }
                }
            } catch (error) {
                log(`Error setting up media elements: ${error.message}`);
            }
        }

        function register() {
            const server = serverInput.value;
            const username = usernameInput.value;
            const password = passwordInput.value;
            const displayName = displayNameInput.value;

            if (!server || !username) {
                showStatus('Please enter server and username', 'error');
                return;
            }

            log('Attempting to register...');

            const uri = SIP.UserAgent.makeURI(`sip:${username}@${server.replace('ws://', '').replace('wss://', '').split('/')[0]}`);

            const userAgentOptions = {
                uri: uri,
                transportOptions: {
                    server: server
                },
                authorizationPassword: password,
                displayName: displayName,
                delegate: {
                    onConnect: () => {
                        log('Connected to server');
                        showStatus('Connected', 'success');
                    },
                    onDisconnect: (error) => {
                        log(`Disconnected: ${error ? error.message : 'Unknown error'}`);
                        showStatus('Disconnected', 'error');
                        resetUI();
                    },
                    onInvite: (invitation) => {
                        log(`Incoming call from ${invitation.remoteIdentity.displayName || invitation.remoteIdentity.uri.user}`);
                        showIncomingCall(invitation);
                    }
                }
            };

            // wire ICE servers configuration if enabled
            try {
                if (iceEnable && iceEnable.checked) {
                    let parsed = null;
                    try {
                        parsed = JSON.parse(iceServersInput.value);
                    } catch (e) {
                        parsed = [{ urls: 'stun:stun.l.google.com:19302' }];
                    }
                    userAgentOptions.sessionDescriptionHandlerFactoryOptions = {
                        peerConnectionConfiguration: { iceServers: parsed }
                    };
                    log(`Using custom ICE servers: ${JSON.stringify(userAgentOptions.sessionDescriptionHandlerFactoryOptions.peerConnectionConfiguration)}`);
                } else {
                    log('ICE servers disabled (using default)');
                }
            } catch (e) {
                log(`Error applying ICE servers config: ${e.message}`);
            }

            userAgent = new SIP.UserAgent(userAgentOptions);
            const registererOptions = {};
            registerer = new SIP.Registerer(userAgent, registererOptions);

            registerer.stateChange.addListener((newState) => {
                log(`Registration state: ${newState}`);
                switch (newState) {
                    case SIP.RegistererState.Registered:
                        showStatus('Registered', 'success');
                        registerBtn.disabled = true;
                        unregisterBtn.disabled = false;
                        callBtn.disabled = false;
                        break;
                    case SIP.RegistererState.Unregistered:
                        showStatus('Unregistered', 'info');
                        break;
                    case SIP.RegistererState.Terminated:
                        showStatus('Registration terminated', 'error');
                        resetUI();
                        break;
                }
            });

            userAgent.start().then(() => {
                log('UserAgent started, attempting registration...');
                return registerer.register();
            }).then(() => {
                log('Registration request sent');
            }).catch((error) => {
                log(`Registration failed: ${error.message}`);
                showStatus('Registration failed', 'error');
                userAgent = null;
            });
        }

        function unregister() {
            if (userAgent) {
                log('Unregistering...');
                if (currentSession) {
                    hangup();
                }

                userAgent.stop().then(() => {
                    log('UserAgent stopped successfully');
                    showStatus('Unregistered', 'info');
                    resetUI();
                }).catch((error) => {
                    log(`Unregister failed: ${error.message}`);
                    resetUI();
                });
            }
        }

        function resetUI() {
            registerBtn.disabled = false;
            unregisterBtn.disabled = true;
            callBtn.disabled = true;
            callControls.classList.remove('active');
            incomingCallDiv.classList.remove('active');
            currentSession = null;
            registerer = null;
            userAgent = null;
            lastOutgoingCallLabel = null;
            resetIceStats();
        }

        function makeCall() {
            const target = callTargetInput.value;
            if (!target || !userAgent) {
                log('Please enter a call target and ensure you are registered');
                return;
            }
            let targetURI = undefined;
            if (target.startsWith('sip:')) {
                targetURI = SIP.UserAgent.makeURI(target);
            } else {
                targetURI = SIP.UserAgent.makeURI(`sip:${target}@${serverInput.value.replace('ws://', '').replace('wss://', '').split('/')[0]}`);
            }

            const inviteOptions = {
                sessionDescriptionHandlerOptions: {
                    constraints: {
                        audio: true,
                        video: false
                    }
                }
            };

            const inviter = new SIP.Inviter(userAgent, targetURI, inviteOptions);
            log(`Calling ${targetURI.toString()}`);
            const outgoingLabel = (targetURI && targetURI.user) ? targetURI.user : target.replace(/^sip:/, '');
            lastOutgoingCallLabel = outgoingLabel || 'unknown';
            inviter._iceLabel = `outgoing:${lastOutgoingCallLabel}`;
            resetIceStats(`<div><strong>Call:</strong> ${inviter._iceLabel}</div><div><strong>Status:</strong> waiting for candidates</div>`);

            // prepare timing container
            inviter._timing = { inviteSent: Date.now() };

            inviter.delegate = {
                onBye: (bye) => {
                    log('Call ended by remote party');
                    endCall();
                },
                onSessionDescriptionHandler: () => {
                    setupMediaElements(inviter);
                    // start monitoring ICE gathering when peerConnection available
                    try { monitorIceGathering(inviter, inviter._iceLabel); } catch (e) { }
                }
            };

            inviter.stateChange.addListener((newState) => {
                log(`Call state: ${newState}`);

                switch (newState) {
                    case SIP.SessionState.Initial:
                        log('Call initializing...');
                        break;
                    case SIP.SessionState.Establishing:
                        callInfo.textContent = `Calling ${target}...`;
                        callControls.classList.add('active');
                        break;
                    case SIP.SessionState.Established:
                        callInfo.textContent = `Connected to ${target}`;
                        setupMediaElements(inviter);
                        // handshake timing: from inviteSent to established
                        try {
                            if (inviter._timing && inviter._timing.inviteSent) {
                                const dur = Date.now() - inviter._timing.inviteSent;
                                const handshakePeer = lastOutgoingCallLabel || target;
                                log(`Outgoing handshake time to ${handshakePeer}: ${formatMs(dur)}`);
                            }
                        } catch (e) { log(`handshake timing error: ${e.message}`); }
                        break;
                    case SIP.SessionState.Terminating:
                        log('Call terminating...');
                        break;
                    case SIP.SessionState.Terminated:
                        log('Call terminated');
                        endCall();
                        break;
                }
            });

            inviter.invite().then(() => {
                currentSession = inviter;
                log('Invite sent successfully');
            }).catch((error) => {
                log(`Failed to send invite: ${error.message}`);
                endCall();
            });
        }

        function showIncomingCall(invitation) {
            currentSession = invitation;
            // record when INVITE was received
            invitation._timing = invitation._timing || {};
            invitation._timing.incomingReceived = Date.now();
            const caller = invitation.remoteIdentity.displayName || invitation.remoteIdentity.uri.user;
            callerInfo.textContent = `Incoming call from: ${caller}`;
            incomingCallDiv.classList.add('active');
            invitation._iceLabel = `incoming:${caller || 'unknown'}`;
            resetIceStats(`<div><strong>Call:</strong> ${invitation._iceLabel}</div><div><strong>Status:</strong> waiting for candidates</div>`);

            invitation.delegate = {
                onSessionDescriptionHandler: () => {
                    setupMediaElements(invitation);
                    try { monitorIceGathering(invitation, invitation._iceLabel); } catch (e) { }
                }
            };

            invitation.stateChange.addListener((newState) => {
                log(`Incoming call state: ${newState}`);

                switch (newState) {
                    case SIP.SessionState.Established:
                        incomingCallDiv.classList.remove('active');
                        callInfo.textContent = `Connected to ${caller}`;
                        callControls.classList.add('active');
                        setupMediaElements(invitation);
                        try {
                            const t = invitation._timing || {};
                            const base = t.answerTime || t.incomingReceived;
                            if (base) {
                                const dur = Date.now() - base;
                                log(`Incoming handshake time from ${caller}: ${formatMs(dur)}`);
                            }
                        } catch (e) { log(`handshake timing error: ${e.message}`); }
                        break;
                    case SIP.SessionState.Terminated:
                        endCall();
                        break;
                }
            });
        }

        function answerCall() {
            if (currentSession) {
                log('Answering incoming call');
                const options = {
                    sessionDescriptionHandlerOptions: {
                        constraints: {
                            audio: true,
                            video: false
                        }
                    }
                };
                // record answer time to measure handshake
                currentSession._timing = currentSession._timing || {};
                currentSession._timing.answerTime = Date.now();
                currentSession.accept(options);
            }
        }

        function rejectCall() {
            if (currentSession) {
                log('Rejecting incoming call');
                currentSession.reject();
                incomingCallDiv.classList.remove('active');
                currentSession = null;
            }
        }

        function hangup() {
            if (currentSession) {
                log('Hanging up call');
                try {
                    switch (currentSession.state) {
                        case SIP.SessionState.Initial:
                        case SIP.SessionState.Establishing:
                            if (currentSession instanceof SIP.Inviter) {
                                currentSession.cancel();
                            } else {
                                currentSession.reject();
                            }
                            break;
                        case SIP.SessionState.Established:
                            currentSession.bye();
                            break;
                        default:
                            log(`Cannot hangup call in state: ${currentSession.state}`);
                            break;
                    }
                } catch (error) {
                    log(`Error hanging up: ${error.message}`);
                    endCall();
                }
            }
        }

        function endCall() {
            callControls.classList.remove('active');
            incomingCallDiv.classList.remove('active');
            currentSession = null;
            isMuted = false;
            isOnHold = false;
            muteBtn.textContent = 'Mute';
            holdBtn.textContent = 'Hold';
            lastOutgoingCallLabel = null;

            // Clean up audio streams
            if (remoteAudio.srcObject) {
                remoteAudio.srcObject.getTracks().forEach(track => track.stop());
                remoteAudio.srcObject = null;
            }
            if (localAudio.srcObject) {
                localAudio.srcObject.getTracks().forEach(track => track.stop());
                localAudio.srcObject = null;
            }

            resetIceStats();
        }

        function toggleMute() {
            if (currentSession && currentSession.sessionDescriptionHandler) {
                try {
                    const senders = currentSession.sessionDescriptionHandler.peerConnection.getSenders();
                    senders.forEach(sender => {
                        if (sender.track && sender.track.kind === 'audio') {
                            sender.track.enabled = isMuted;
                        }
                    });
                    isMuted = !isMuted;
                    muteBtn.textContent = isMuted ? 'Unmute' : 'Mute';
                    log(isMuted ? 'Audio muted' : 'Audio unmuted');
                } catch (error) {
                    log(`Error toggling mute: ${error.message}`);
                }
            } else {
                log('No active session for mute operation');
            }
        }

        function toggleHold() {
            if (currentSession) {
                if (isOnHold) {
                    currentSession.unhold().then(() => {
                        isOnHold = false;
                        holdBtn.textContent = 'Hold';
                        log('Call is resumed from hold');
                    }).catch(error => {
                        log(`Failed to unhold: ${error.message}`);
                    });
                } else {
                    currentSession.hold().then(() => {
                        isOnHold = true;
                        holdBtn.textContent = 'Unhold';
                        log('Call is on hold');
                    }).catch(error => {
                        log(`Failed to hold: ${error.message}`);
                    });
                }
            }
        }

        // Initialize
        loadDefaultIceServers();
        log('SIP Phone initialized');
    </script>
</body>

</html>